/*
 * Copyright (C) 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.tools.ringtone.soundfile;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.HashMap;

/**
 * CheapAAC is a CheapSoundFile implementation for AAC (Advanced Audio Codec)
 * encoded sound files. It supports files with an MP4 header, including
 * unencrypted files encoded by Apple iTunes, and also files with a more basic
 * ADTS header.
 */
public class CheapAAC extends CheapSoundFile {
	public static Factory getFactory() {
		return new Factory() {
			public CheapSoundFile create() {
				return new CheapAAC();
			}

			public String[] getSupportedExtensions() {
				return new String[] { "aac", "m4a" };
			}
		};
	}

	class Atom {
		public int start;
		public int len; // including header
		public byte[] data;
	};

	public static final int kDINF = 0x64696e66;
	public static final int kFTYP = 0x66747970;
	public static final int kHDLR = 0x68646c72;
	public static final int kMDAT = 0x6d646174;
	public static final int kMDHD = 0x6d646864;
	public static final int kMDIA = 0x6d646961;
	public static final int kMINF = 0x6d696e66;
	public static final int kMOOV = 0x6d6f6f76;
	public static final int kMP4A = 0x6d703461;
	public static final int kMVHD = 0x6d766864;
	public static final int kSMHD = 0x736d6864;
	public static final int kSTBL = 0x7374626c;
	public static final int kSTCO = 0x7374636f;
	public static final int kSTSC = 0x73747363;
	public static final int kSTSD = 0x73747364;
	public static final int kSTSZ = 0x7374737a;
	public static final int kSTTS = 0x73747473;
	public static final int kTKHD = 0x746b6864;
	public static final int kTRAK = 0x7472616b;

	public static final int[] kRequiredAtoms = { kDINF, kHDLR, kMDHD, kMDIA,
			kMINF, kMOOV, kMVHD, kSMHD, kSTBL, kSTSD, kSTSZ, kSTTS, kTKHD,
			kTRAK, };

	public static final int[] kSaveDataAtoms = { kDINF, kHDLR, kMDHD, kMVHD,
			kSMHD, kTKHD, kSTSD, };

	// Member variables containing frame info
	private int mNumFrames;
	private int[] mFrameOffsets;
	private int[] mFrameLens;
	private int[] mFrameGains;
	private int mFileSize;
	private HashMap<Integer, Atom> mAtomMap;

	// Member variables containing sound file info
	private int mBitrate;
	private int mSampleRate;
	private int mChannels;
	private int mSamplesPerFrame;

	// Member variables used only while initially parsing the file
	private int mOffset;
	private int mMinGain;
	private int mMaxGain;
	private int mMdatOffset;
	private int mMdatLength;

	public CheapAAC() {
	}

	@Override
	public int getNumFrames() {
		return mNumFrames;
	}

	@Override
	public int getSamplesPerFrame() {
		return mSamplesPerFrame;
	}

	@Override
	public int[] getFrameOffsets() {
		return mFrameOffsets;
	}

	@Override
	public int[] getFrameLens() {
		return mFrameLens;
	}

	@Override
	public int[] getFrameGains() {
		return mFrameGains;
	}

	@Override
	public int getFileSizeBytes() {
		return mFileSize;
	}

	@Override
	public int getAvgBitrateKbps() {
		return mFileSize / (mNumFrames * mSamplesPerFrame);
	}

	@Override
	public int getSampleRate() {
		return mSampleRate;
	}

	@Override
	public int getChannels() {
		return mChannels;
	}

	@Override
	public String getFiletype() {
		return "AAC";
	}

	public String atomToString(int atomType) {
		String str = "";
		str += (char) ((atomType >> 24) & 0xff);
		str += (char) ((atomType >> 16) & 0xff);
		str += (char) ((atomType >> 8) & 0xff);
		str += (char) (atomType & 0xff);
		return str;
	}

	@Override
	public void ReadFile(File inputFile) throws java.io.FileNotFoundException,
			java.io.IOException {
		super.ReadFile(inputFile);
		mChannels = 0;
		mSampleRate = 0;
		mBitrate = 0;
		mSamplesPerFrame = 0;
		mNumFrames = 0;
		mMinGain = 255;
		mMaxGain = 0;
		mOffset = 0;
		mMdatOffset = -1;
		mMdatLength = -1;

		mAtomMap = new HashMap<Integer, Atom>();

		// No need to handle filesizes larger than can fit in a 32-bit int
		mFileSize = (int) mInputFile.length();

		/* System.out.println("File size = " + mFileSize); */

		if (mFileSize < 128) {
			throw new java.io.IOException("File too small to parse");
		}

		// Read the first 8 bytes
		FileInputStream stream = new FileInputStream(mInputFile);
		byte[] header = new byte[8];
		stream.read(header, 0, 8);

		if (header[0] == 0 && header[4] == 'f' && header[5] == 't'
				&& header[6] == 'y' && header[7] == 'p') {
			// Create a new stream, reset to the beginning of the file
			stream = new FileInputStream(mInputFile);
			parseMp4(stream, mFileSize);
		} else {
			throw new java.io.IOException("Unknown file format");
		}

		if (mMdatOffset > 0 && mMdatLength > 0) {
			stream = new FileInputStream(mInputFile);
			stream.skip(mMdatOffset);
			mOffset = mMdatOffset;
			parseMdat(stream, mMdatLength);
		} else {
			throw new java.io.IOException("Didn't find mdat");
		}

		/*
		 * for (int i = 0; i < mNumFrames; i++) { System.out.println("Gain " + i
		 * + ": " + mFrameGains[i]); }
		 */

		/*
		 * System.out.println("Atoms found:"); for (int atomType :
		 * mAtomMap.keySet()) { System.out.println("    " +
		 * atomToString(atomType)); }
		 */

		boolean bad = false;
		for (int requiredAtomType : kRequiredAtoms) {
			if (!mAtomMap.containsKey(requiredAtomType)) {
				System.out.println("Missing atom: "
						+ atomToString(requiredAtomType));
				bad = true;
			}
		}

		if (bad) {
			throw new java.io.IOException("Could not parse MP4 file");
		}
	}

	private void parseMp4(InputStream stream, int maxLen)
			throws java.io.IOException {
		/* System.out.println("parseMp4 maxLen = " + maxLen); */

		while (maxLen > 8) {
			int initialOffset = mOffset;

			byte[] atomHeader = new byte[8];
			stream.read(atomHeader, 0, 8);
			int atomLen = ((0xff & atomHeader[0]) << 24)
					| ((0xff & atomHeader[1]) << 16)
					| ((0xff & atomHeader[2]) << 8) | ((0xff & atomHeader[3]));
			/*
			 * System.out.println("atomType = " + (char)atomHeader[4] +
			 * (char)atomHeader[5] + (char)atomHeader[6] + (char)atomHeader[7] +
			 * "  " + "offset = " + mOffset + "  " + "atomLen = " + atomLen);
			 */
			if (atomLen > maxLen)
				atomLen = maxLen;
			int atomType = ((0xff & atomHeader[4]) << 24)
					| ((0xff & atomHeader[5]) << 16)
					| ((0xff & atomHeader[6]) << 8) | ((0xff & atomHeader[7]));

			Atom atom = new Atom();
			atom.start = mOffset;
			atom.len = atomLen;
			mAtomMap.put(atomType, atom);

			mOffset += 8;

			if (atomType == kMOOV || atomType == kTRAK || atomType == kMDIA
					|| atomType == kMINF || atomType == kSTBL) {
				parseMp4(stream, atomLen);
			} else if (atomType == kSTSZ) {
				parseStsz(stream, atomLen - 8);
			} else if (atomType == kSTTS) {
				parseStts(stream, atomLen - 8);
			} else if (atomType == kMDAT) {
				mMdatOffset = mOffset;
				mMdatLength = atomLen - 8;
			} else {
				for (int savedAtomType : kSaveDataAtoms) {
					if (savedAtomType == atomType) {
						byte[] data = new byte[atomLen - 8];
						stream.read(data, 0, atomLen - 8);
						mOffset += atomLen - 8;
						mAtomMap.get(atomType).data = data;
					}
				}
			}

			if (atomType == kSTSD) {
				parseMp4aFromStsd();
			}

			maxLen -= atomLen;
			int skipLen = atomLen - (mOffset - initialOffset);
			/* System.out.println("* atomLen: " + atomLen); */
			/* System.out.println("* mOffset: " + mOffset); */
			/* System.out.println("* initialOffset: " + initialOffset); */
			/* System.out.println("*   diff: " + (mOffset - initialOffset)); */
			/* System.out.println("* skipLen: " + skipLen); */

			if (skipLen < 0) {
				throw new java.io.IOException("Went over by " + (-skipLen)
						+ " bytes");
			}

			stream.skip(skipLen);
			mOffset += skipLen;
		}
	}

	void parseStts(InputStream stream, int maxLen) throws java.io.IOException {
		byte[] sttsData = new byte[16];
		stream.read(sttsData, 0, 16);
		mOffset += 16;
		mSamplesPerFrame = ((0xff & sttsData[12]) << 24)
				| ((0xff & sttsData[13]) << 16) | ((0xff & sttsData[14]) << 8)
				| ((0xff & sttsData[15]));

		/* System.out.println("STTS samples per frame: " + mSamplesPerFrame); */
	}

	void parseStsz(InputStream stream, int maxLen) throws java.io.IOException {
		byte[] stszHeader = new byte[12];
		stream.read(stszHeader, 0, 12);
		mOffset += 12;
		mNumFrames = ((0xff & stszHeader[8]) << 24)
				| ((0xff & stszHeader[9]) << 16)
				| ((0xff & stszHeader[10]) << 8) | ((0xff & stszHeader[11]));
		/* System.out.println("mNumFrames = " + mNumFrames); */

		mFrameOffsets = new int[mNumFrames];
		mFrameLens = new int[mNumFrames];
		mFrameGains = new int[mNumFrames];
		byte[] frameLenBytes = new byte[4 * mNumFrames];
		stream.read(frameLenBytes, 0, 4 * mNumFrames);
		mOffset += 4 * mNumFrames;
		for (int i = 0; i < mNumFrames; i++) {
			mFrameLens[i] = ((0xff & frameLenBytes[4 * i + 0]) << 24)
					| ((0xff & frameLenBytes[4 * i + 1]) << 16)
					| ((0xff & frameLenBytes[4 * i + 2]) << 8)
					| ((0xff & frameLenBytes[4 * i + 3]));
			/* System.out.println("FrameLen[" + i + "] = " + mFrameLens[i]); */
		}
	}

	void parseMp4aFromStsd() {
		byte[] stsdData = mAtomMap.get(kSTSD).data;
		mChannels = ((0xff & stsdData[32]) << 8) | ((0xff & stsdData[33]));
		mSampleRate = ((0xff & stsdData[40]) << 8) | ((0xff & stsdData[41]));

		/*
		 * System.out.println("%% channels = " + mChannels + ", " +
		 * "sampleRate = " + mSampleRate);
		 */
	}

	void parseMdat(InputStream stream, int maxLen) throws java.io.IOException {
		/* System.out.println("***MDAT***"); */
		int initialOffset = mOffset;
		for (int i = 0; i < mNumFrames; i++) {
			mFrameOffsets[i] = mOffset;
			/* System.out.println("&&& start: " + (mOffset - initialOffset)); */
			/*
			 * System.out.println("&&& start + len: " + (mOffset - initialOffset
			 * + mFrameLens[i]));
			 */
			/* System.out.println("&&& maxLen: " + maxLen); */

			if (mOffset - initialOffset + mFrameLens[i] > maxLen - 8) {
				mFrameGains[i] = 0;
			} else {
				readFrameAndComputeGain(stream, i);
			}
			if (mFrameGains[i] < mMinGain)
				mMinGain = mFrameGains[i];
			if (mFrameGains[i] > mMaxGain)
				mMaxGain = mFrameGains[i];

			if (mProgressListener != null) {
				boolean keepGoing = mProgressListener.reportProgress(mOffset
						* 1.0 / mFileSize);
				if (!keepGoing) {
					break;
				}
			}
		}
	}

	void readFrameAndComputeGain(InputStream stream, int frameIndex)
			throws java.io.IOException {

		if (mFrameLens[frameIndex] < 4) {
			mFrameGains[frameIndex] = 0;
			stream.skip(mFrameLens[frameIndex]);
			return;
		}

		int initialOffset = mOffset;

		byte[] data = new byte[4];
		stream.read(data, 0, 4);
		mOffset += 4;

		/*
		 * System.out.println( "Block " + frameIndex + ": " + data[0] + " " +
		 * data[1] + " " + data[2] + " " + data[3]);
		 */

		int idSynEle = (0xe0 & data[0]) >> 5;
		/* System.out.println("idSynEle = " + idSynEle); */

		switch (idSynEle) {
		case 0: // ID_SCE: mono
			int monoGain = ((0x01 & data[0]) << 7) | ((0xfe & data[1]) >> 1);
			/* System.out.println("monoGain = " + monoGain); */
			mFrameGains[frameIndex] = monoGain;
			break;
		case 1: // ID_CPE: stereo
			int windowSequence = (0x60 & data[1]) >> 5;
			/* System.out.println("windowSequence = " + windowSequence); */
			int windowShape = (0x10 & data[1]) >> 4;
			/* System.out.println("windowShape = " + windowShape); */

			int maxSfb;
			int scaleFactorGrouping;
			int maskPresent;
			int startBit;

			if (windowSequence == 2) {
				maxSfb = 0x0f & data[1];

				scaleFactorGrouping = (0xfe & data[2]) >> 1;

				maskPresent = ((0x01 & data[2]) << 1) | ((0x80 & data[3]) >> 7);

				startBit = 25;
			} else {
				maxSfb = ((0x0f & data[1]) << 2) | ((0xc0 & data[2]) >> 6);

				scaleFactorGrouping = -1;

				maskPresent = (0x18 & data[2]) >> 3;

				startBit = 21;
			}

			/* System.out.println("maxSfb = " + maxSfb); */
			/*
			 * System.out.println("scaleFactorGrouping = " +
			 * scaleFactorGrouping);
			 */
			/* System.out.println("maskPresent = " + maskPresent); */
			/* System.out.println("startBit = " + startBit); */

			if (maskPresent == 1) {
				int sfgZeroBitCount = 0;
				for (int b = 0; b < 7; b++) {
					if ((scaleFactorGrouping & (1 << b)) == 0) {
						/*
						 * System.out.println("  1 point for bit " + b + ": " +
						 * (1 << b) + ", " + (scaleFactorGrouping & (1 << b)));
						 */
						sfgZeroBitCount++;
					}
				}
				/* System.out.println("sfgZeroBitCount = " + sfgZeroBitCount); */

				int numWindowGroups = 1 + sfgZeroBitCount;
				/* System.out.println("numWindowGroups = " + numWindowGroups); */

				int skip = maxSfb * numWindowGroups;
				/* System.out.println("skip = " + skip); */

				startBit += skip;
				/* System.out.println("new startBit = " + startBit); */
			}

			// We may need to fill our buffer with more than the 4
			// bytes we've already read, here.
			int bytesNeeded = 1 + ((startBit + 7) / 8);
			byte[] oldData = data;
			data = new byte[bytesNeeded];
			data[0] = oldData[0];
			data[1] = oldData[1];
			data[2] = oldData[2];
			data[3] = oldData[3];
			stream.read(data, 4, bytesNeeded - 4);
			mOffset += (bytesNeeded - 4);
			/* System.out.println("bytesNeeded: " + bytesNeeded); */

			int firstChannelGain = 0;
			for (int b = 0; b < 8; b++) {
				int b0 = (b + startBit) / 8;
				int b1 = 7 - ((b + startBit) % 8);
				int add = (((1 << b1) & data[b0]) >> b1) << (7 - b);
				/*
				 * System.out.println("Bit " + (b + startBit) + " " + "b0 " + b0
				 * + " " + "b1 " + b1 + " " + "add " + add);
				 */
				firstChannelGain += add;
			}
			/* System.out.println("firstChannelGain = " + firstChannelGain); */

			mFrameGains[frameIndex] = firstChannelGain;
			break;

		default:
			mFrameGains[frameIndex] = 0;
			/* System.out.println("Unhandled idSynEle"); */
			break;
		}

		int skip = mFrameLens[frameIndex] - (mOffset - initialOffset);
		/* System.out.println("frameLen = " + mFrameLens[frameIndex]); */
		/* System.out.println("Skip = " + skip); */

		stream.skip(skip);
		mOffset += skip;
	}

	public void StartAtom(FileOutputStream out, int atomType)
			throws java.io.IOException {
		byte[] atomHeader = new byte[8];
		int atomLen = mAtomMap.get(atomType).len;
		atomHeader[0] = (byte) ((atomLen >> 24) & 0xff);
		atomHeader[1] = (byte) ((atomLen >> 16) & 0xff);
		atomHeader[2] = (byte) ((atomLen >> 8) & 0xff);
		atomHeader[3] = (byte) (atomLen & 0xff);
		atomHeader[4] = (byte) ((atomType >> 24) & 0xff);
		atomHeader[5] = (byte) ((atomType >> 16) & 0xff);
		atomHeader[6] = (byte) ((atomType >> 8) & 0xff);
		atomHeader[7] = (byte) (atomType & 0xff);
		out.write(atomHeader, 0, 8);
	}

	public void WriteAtom(FileOutputStream out, int atomType)
			throws java.io.IOException {
		Atom atom = mAtomMap.get(atomType);
		StartAtom(out, atomType);
		out.write(atom.data, 0, atom.len - 8);
	}

	public void SetAtomData(int atomType, byte[] data) {
		Atom atom = mAtomMap.get(atomType);
		if (atom == null) {
			atom = new Atom();
			mAtomMap.put(atomType, atom);
		}
		atom.len = data.length + 8;
		atom.data = data;
	}

	@Override
	public void WriteFile(File outputFile, int startFrame, int numFrames)
			throws java.io.IOException {
		outputFile.createNewFile();
		FileInputStream in = new FileInputStream(mInputFile);
		FileOutputStream out = new FileOutputStream(outputFile);

		SetAtomData(kFTYP, new byte[] { 'M', '4', 'A', ' ', 0, 0, 0, 0, 'M',
				'4', 'A', ' ', 'm', 'p', '4', '2', 'i', 's', 'o', 'm', 0, 0, 0,
				0 });

		SetAtomData(kSTTS, new byte[] {
				0,
				0,
				0,
				0, // version / flags
				0,
				0,
				0,
				1, // entry count
				(byte) ((numFrames >> 24) & 0xff),
				(byte) ((numFrames >> 16) & 0xff),
				(byte) ((numFrames >> 8) & 0xff), (byte) (numFrames & 0xff),
				(byte) ((mSamplesPerFrame >> 24) & 0xff),
				(byte) ((mSamplesPerFrame >> 16) & 0xff),
				(byte) ((mSamplesPerFrame >> 8) & 0xff),
				(byte) (mSamplesPerFrame & 0xff) });

		SetAtomData(kSTSC, new byte[] { 0,
				0,
				0,
				0, // version / flags
				0,
				0,
				0,
				1, // entry count
				0,
				0,
				0,
				1, // first chunk
				(byte) ((numFrames >> 24) & 0xff),
				(byte) ((numFrames >> 16) & 0xff),
				(byte) ((numFrames >> 8) & 0xff), (byte) (numFrames & 0xff), 0,
				0, 0, 1 // Smaple desc index
				});

		byte[] stszData = new byte[12 + 4 * numFrames];
		stszData[8] = (byte) ((numFrames >> 24) & 0xff);
		stszData[9] = (byte) ((numFrames >> 16) & 0xff);
		stszData[10] = (byte) ((numFrames >> 8) & 0xff);
		stszData[11] = (byte) (numFrames & 0xff);
		for (int i = 0; i < numFrames; i++) {
			stszData[12 + 4 * i] = (byte) ((mFrameLens[startFrame + i] >> 24) & 0xff);
			stszData[13 + 4 * i] = (byte) ((mFrameLens[startFrame + i] >> 16) & 0xff);
			stszData[14 + 4 * i] = (byte) ((mFrameLens[startFrame + i] >> 8) & 0xff);
			stszData[15 + 4 * i] = (byte) (mFrameLens[startFrame + i] & 0xff);
		}
		SetAtomData(kSTSZ, stszData);

		int mdatOffset = 144 + 4 * numFrames + mAtomMap.get(kSTSD).len
				+ mAtomMap.get(kSTSC).len + mAtomMap.get(kMVHD).len
				+ mAtomMap.get(kTKHD).len + mAtomMap.get(kMDHD).len
				+ mAtomMap.get(kHDLR).len + mAtomMap.get(kSMHD).len
				+ mAtomMap.get(kDINF).len;

		/* System.out.println("Mdat offset: " + mdatOffset); */

		SetAtomData(kSTCO,
				new byte[] { 0,
						0,
						0,
						0, // version / flags
						0,
						0,
						0,
						1, // entry count
						(byte) ((mdatOffset >> 24) & 0xff),
						(byte) ((mdatOffset >> 16) & 0xff),
						(byte) ((mdatOffset >> 8) & 0xff),
						(byte) (mdatOffset & 0xff), });

		mAtomMap.get(kSTBL).len = 8 + mAtomMap.get(kSTSD).len
				+ mAtomMap.get(kSTTS).len + mAtomMap.get(kSTSC).len
				+ mAtomMap.get(kSTSZ).len + mAtomMap.get(kSTCO).len;

		mAtomMap.get(kMINF).len = 8 + mAtomMap.get(kDINF).len
				+ mAtomMap.get(kSMHD).len + mAtomMap.get(kSTBL).len;

		mAtomMap.get(kMDIA).len = 8 + mAtomMap.get(kMDHD).len
				+ mAtomMap.get(kHDLR).len + mAtomMap.get(kMINF).len;

		mAtomMap.get(kTRAK).len = 8 + mAtomMap.get(kTKHD).len
				+ mAtomMap.get(kMDIA).len;

		mAtomMap.get(kMOOV).len = 8 + mAtomMap.get(kMVHD).len
				+ mAtomMap.get(kTRAK).len;

		int mdatLen = 8;
		for (int i = 0; i < numFrames; i++) {
			mdatLen += mFrameLens[startFrame + i];
		}
		mAtomMap.get(kMDAT).len = mdatLen;

		WriteAtom(out, kFTYP);
		StartAtom(out, kMOOV);
		{
			WriteAtom(out, kMVHD);
			StartAtom(out, kTRAK);
			{
				WriteAtom(out, kTKHD);
				StartAtom(out, kMDIA);
				{
					WriteAtom(out, kMDHD);
					WriteAtom(out, kHDLR);
					StartAtom(out, kMINF);
					{
						WriteAtom(out, kDINF);
						WriteAtom(out, kSMHD);
						StartAtom(out, kSTBL);
						{
							WriteAtom(out, kSTSD);
							WriteAtom(out, kSTTS);
							WriteAtom(out, kSTSC);
							WriteAtom(out, kSTSZ);
							WriteAtom(out, kSTCO);
						}
					}
				}
			}
		}
		StartAtom(out, kMDAT);

		int maxFrameLen = 0;
		for (int i = 0; i < numFrames; i++) {
			if (mFrameLens[startFrame + i] > maxFrameLen)
				maxFrameLen = mFrameLens[startFrame + i];
		}
		byte[] buffer = new byte[maxFrameLen];
		int pos = 0;
		for (int i = 0; i < numFrames; i++) {
			int skip = mFrameOffsets[startFrame + i] - pos;
			int len = mFrameLens[startFrame + i];
			if (skip < 0) {
				continue;
			}
			if (skip > 0) {
				in.skip(skip);
				pos += skip;
			}
			in.read(buffer, 0, len);
			out.write(buffer, 0, len);
			pos += len;
		}

		in.close();
		out.close();
	}

	/**
	 * For debugging public static void main(String[] argv) throws Exception {
	 * File f = new File(""); CheapAAC c = new CheapAAC(); c.ReadFile(f);
	 * c.WriteFile(new File(""), 0, c.getNumFrames()); }
	 **/
};
